#include "url.h"
#include "Shlwapi.h"
#include <curl/curl.h>
#include <string.h>
#include <stdio.h>

///////////////////////////////////////////////////////////////////////////////

// #define FILE_TRACE

#include "file_trace.h"

///////////////////////////////////////////////////////////////////////////////

//------------------------------------------------------------------------------
// structs

//------------------------------------------------------------------------------
/**
 * @struct memoryex_t
 *         This struct is declared simply to extract content length and allocate
 *         memory at once instead of piecemeal allocations for better performance of
 *         s_WriteMemoryCallback().
 */
typedef struct memoryex
{
    memory_t* memory;
    int       content_length;
} memoryex_t;

//------------------------------------------------------------------------------
// static functions prototypes
//------------------------------------------------------------------------------

#ifdef _DEBUG

static int
s_DbgCallback(CURL* a_handle, curl_infotype a_type, char* a_data, size_t a_size, void* a_user);

static void
s_GetInfo(CURL* a_handle);

#endif

static bool
s_AllocateMemory(const char* a_data, memoryex_t* a_memoryex);

static size_t
s_GetHeaderCallback(void* a_data, size_t a_size, size_t a_count, void* a_cookie);

static size_t
s_WriteMemoryCallback(void* a_ptr, size_t a_size, size_t a_count, void* a_data);

//------------------------------------------------------------------------------
// public functions
//------------------------------------------------------------------------------

bool
URL_DownloadToMemory(memory_t* a_memory, int a_timeout_connection, int a_timeout)
{
    CURL*  handle = NULL;
    bool  result = false;

    F_PRINT(("start\n"));

    // check arguments
    if (a_memory == NULL)
    {
        F_ERROR(("argument is null!\n"));
        goto exit;
    }

    if (a_timeout_connection < 0 || a_timeout < 0)
    {
        F_ERROR(("negative number!\n"));
        goto exit;
    }

    // launch curl library
    handle = curl_easy_init();
    if (handle == NULL)
    {
        F_ERROR(("no cUrl handle!\n"));
        goto exit;
    }

    F_PRINT(("url:|%s|\n", a_memory->url));

    // get to memory
    curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1);
    curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1);
    curl_easy_setopt(handle, CURLOPT_CONNECTTIMEOUT, a_timeout_connection);
    curl_easy_setopt(handle, CURLOPT_TIMEOUT, a_timeout);
#ifdef _DEBUG
    curl_easy_setopt(handle, CURLOPT_VERBOSE);
    curl_easy_setopt(handle, CURLOPT_DEBUGFUNCTION, s_DbgCallback);
#endif
    curl_easy_setopt(handle, CURLOPT_URL, a_memory->url);
    curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, s_GetHeaderCallback);
    curl_easy_setopt(handle, CURLOPT_WRITEHEADER, &a_memory);
    curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, s_WriteMemoryCallback);
    curl_easy_setopt(handle, CURLOPT_WRITEDATA, &a_memory);
    curl_easy_setopt(handle, CURLOPT_USERAGENT, "libcurl-agent/1.0");

    if (CURLE_OK == curl_easy_perform(handle))
    {
        F_PRINT(("OK...\n"));
        result = true;
    }
    else
    {
        F_PRINT(("Failed!\n"));
    }

#ifdef _DEBUG
    s_GetInfo(handle);
#endif

    // clean-up
    curl_easy_cleanup(handle);

    F_PRINT(("completed with memory size and memory:|%d|\n"
             "|%s|\n\n",
             a_memory->size, a_memory->retrieved));

exit:

    return result;
}

//------------------------------------------------------------------------------
// static functions
//------------------------------------------------------------------------------

#ifdef _DEBUG

static int
s_DbgCallback(CURL* a_handle, curl_infotype a_type, char* a_data, size_t a_size, void* a_user)
{
    // ignore other types!
    if (a_type == CURLINFO_TEXT)
        F_PRINT(("size and data:|%d|%s|\n", a_size, a_data));

    return a_size;
};

static void
s_GetInfo(CURL* a_handle)
{
    double  total_time = 0;
    double  download_size = 0;
    double  download_speed = 0;

    if (CURLE_OK != curl_easy_getinfo(a_handle, CURLINFO_TOTAL_TIME, &total_time))
        total_time = -1;

    if (CURLE_OK != curl_easy_getinfo(a_handle, CURLINFO_SIZE_DOWNLOAD, &download_size))
        download_size = -1;

    if (CURLE_OK != curl_easy_getinfo(a_handle, CURLINFO_SPEED_DOWNLOAD, &download_speed))
        download_speed = -1;

    F_PRINT(("\n"
             "    total time    :|%f| sec\n"
             "    download size :|%.0f| bytes\n"
             "    download speed:|%.0f| bytes/sec\n",
             total_time, download_size, download_speed));
}

#endif

static bool
s_AllocateMemory(const char* a_data, memoryex_t* a_memoryex)
{
    bool  result = false;

    // check!
    if (strstr(a_data, "Content-Length:") == NULL)
        goto exit;

    a_memoryex->content_length = atoi(a_data + 15);   // 15 = strlen("Content-Length:");

    F_PRINT(("content length:|%d|\n", a_memoryex->content_length));

    // try to allocate memory for content
    if (a_memoryex->content_length != 0)
    {
        a_memoryex->memory->retrieved = (char*) malloc(a_memoryex->content_length + 1);
        if (a_memoryex->memory->retrieved == NULL)
        {
            a_memoryex->content_length = 0;
            F_ERROR(("malloc() failed!\n"));
        }
        else
        {
            result = true;
        }
    }

exit:

    return result;
}

static size_t
s_GetHeaderCallback(void* a_data, size_t a_size, size_t a_count, void* a_cookie)
{
    char  data[URL_SIZE] = {0};
    size_t  size = a_size * a_count;
    memoryex_t*  memoryex = (memoryex_t*) a_cookie;

    F_PRINT(("size:|%d|\n", size));

    // data is not expected to be null terminated always!
    _snprintf(data, URL_SIZE - 1, "%s", (char*) a_data);

    F_PRINT(("data:|%s|\n", data));

    // currently we are interested only in allocating memory according to content_length!
    s_AllocateMemory(data, memoryex);

    return size;
}

static size_t
s_WriteMemoryCallback(void* a_ptr, size_t a_size, size_t a_count, void* a_data)
{
    memoryex_t*  memoryex = (memoryex_t*) a_data;
    size_t  size = a_size * a_count;

    F_PRINT(("content_length:|%d|  size:|%d|\n", memoryex->content_length, size));

    // should increase allocated memory?
    if (memoryex->content_length == 0)
    {
        memoryex->memory->retrieved = (char*) realloc(memoryex->memory->retrieved, memoryex->memory->size + size + 1);
        if (memoryex->memory->retrieved == NULL)
        {
            F_PRINT(("ralloc failed"));
            size = 0;
            goto exit;
        }

        F_PRINT(("memory allocated...\n"));
    }
    else
    {
        F_PRINT(("no memory allocated...\n"));
    } // if

    memcpy(&(memoryex->memory->retrieved[memoryex->memory->size]), a_ptr, size);
    memoryex->memory->size += size;
    memoryex->memory->retrieved[memoryex->memory->size] = '\0';

    F_PRINT(("size new and cumulative:|%d|%d|\n", size, memoryex->memory->size));

exit:

    return size;
}
